/*
 * SRMI (an easy-to-use zero-code remoting library for Java)
 * Copyright (C) 2007  Mohammad Nabil Hussein
 * mohammad (dot) nabil (dot) h (at) gmail (dot) com
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

package SRMI.RemotingServices.ClassGeneration;

import SRMI.SRMIException;
import SRMI.Tests.SRMIClassLoader;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.RandomAccessFile;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.channels.FileLock;
import java.util.ArrayList;
import java.util.HashSet;
import org.apache.log4j.Logger;

/**
 * Abstract class generator to be subclassed by other class generators. 
 * Provides common utilities, and a common interface. Also handles locking
 * (synchronization between several class generators running at the same time;
 * This can happen if multiple clients or servers are booting up in the same
 * time and in the same working directory; on the same machine). 
 * 
 * Handles detecting out-of-date classes, so as to prevent regenerating 
 * classes that wasn't changed. This class also handles that several 
 * generated classes might need to be compiled together because of
 * mutual dependency.
 * @author meemo
 */
public abstract class ClassGenerator
{
    private final Logger l = Logger.getLogger(ClassGenerator.class);
    
    public ClassGenerator()
    {
    }
    
    static
    {
        // assure the lock file exists
        try
        {
            new File("javac_lock").createNewFile();
        }
        catch (Exception ex)
        {}
    }
    
    // used for both .java and .class files, cuz need for locking of both
    // are mutually exclusive (i'll never need to lock both in the same time)
    private FileLock lock;
    
    private static HashSet<String> onProgress = new HashSet<String>();
    
    // this variable is used when regenerating classes, 
    // the file states appears that the file is being generated by another JVM/generator
    // if the boolean is set, then the generator is us, hence we can proceed with the
    // generation
    private boolean regenerate = false;
    
    /**
     * Prepare the generated class (Emits the file only. Doesn't load the
     * generated class).
     */
    public void Prepare() throws SRMIException
    {
        synchronized (getWrappedClass())
        {
            PrepareEmit();
            PrepareFolderHeirarchy();
                        
            if(classPath                == null
                    || classesFolder            == null
                    || destinationFolder        == null
                    || fullPathToSourceFileName == null
                    || fullPathOfGeneratedClassFile == null
                    || generatedClassName       == null
                    || package_                 == null)
            {
                l.error("Prepare emit didn't prepare all required variables");
                return;
            }
            
            if(!regenerate && AlreadyInProgress())
                return;
            else
                AddInProgress();
            
            l.trace("Writing code for " + generatedClassName);
            
            OpenFile();
            
            if(file != null)
            {
                EmitPackage();
                EmitClass();
                
                CloseFile();
            }
            else
            {
                l.info(generatedClassName + ".java already exists, skipping");
            }
            
            RemoveFromProgress();
        }
    }
    
    /**
     * Loads the generated class file. Checks if out-of-date. Handles sync with other
     * generators. Calls Perpare() if not already generated, or if needs regeneration.
     * @return
     */
    public Class<?> GetGenerated() throws SRMIException
    {
        PrepareEmit();
        PrepareFolderHeirarchy();
        
        String fullPathOfDependencyClassFile = destinationFolder +
                    package_.replace(".",java.lang.System.getProperty("file.separator")) +
                    java.lang.System.getProperty("file.separator") +
                    wrappedUnQualifiedClassName +
                    ".class";
                
        ClassLoader loader = null;
        try
        {            
            if(SRMIClassLoader.getInstance() != null)
                loader = SRMIClassLoader.getInstance();
            else
                loader = new URLClassLoader(new URL[]{new File(destinationFolder).toURL()});
            l.info("Using class loader : " + loader);
        }
        catch(MalformedURLException ex)
        {
            l.error("Can't create URLClassLoader to load the generated classes");
            throw new SRMIException("Can't create URLClassLoader to load the generated classes",ex);
        }

        try
        {
            Class ret = loader.loadClass(package_ + "." + generatedClassName);
                    //Class.forName(package_ + "." + generatedClassName);
            
            assert ret != null : "the class laoder " + loader + " have loaded a null class !";
            
            l.info(generatedClassName + " is already in cache, checking if up-to-date...");          
                        
            if(new File(fullPathOfDependencyClassFile).lastModified() >
                    new File(fullPathOfGeneratedClassFile).lastModified())
            {
                l.info("Out-of-date, regenerating");
                // must delete the class file so other jvm's know we are
                // on progress
                DeleteOld(new File(fullPathOfGeneratedClassFile));
                
                File srcFile = new File(fullPathToSourceFileName);
                if(srcFile.exists())
                {
                    // must delete it because i never overwrite a source file
                    // so i allow re-generation of source files
                    DeleteOld(srcFile);
                }
                try
                {
                    // must create it again so other jvms know we are generating it
                    srcFile.createNewFile();
                    
                    // override system-wide lock on that file
                    regenerate = true;
                } catch (IOException ex)
                {
                    l.error("",ex);
                }
            }
            else
            {
                l.info("Up-to-date, reusing (didn't consider other classes dependencies)");
                return ret;
            }
        }
        catch(ClassNotFoundException ex)
        { l.info(generatedClassName + " not found in cahce, generating");
        
          // copied code from above block\
          // this code removes the source file if exists ( so time stamp is after the new dependency class file in all cases )
          // so we guarantee the src file will be included in the javac file list ( file are time stamp filtered )
        File srcFile = new File(fullPathToSourceFileName);
                if(srcFile.exists())
                {
                    // must delete it because i never overwrite a source file
                    // so i allow re-generation of source files
                    DeleteOld(srcFile);
                }
                try
                {
                    // must create it again so other jvms know we are generating it
                    srcFile.createNewFile();
                    
                    // override system-wide lock on that file
                    regenerate = true;
                } catch (IOException ex2)
                {
                    l.error("Exception creating file " + srcFile.toString(),ex2);
                }
        }
        
        ClassGenerationUtils.AssertMeetsSrmiCriteria(wrappedClass);
        
        Prepare();
        
        // before compiling progress in writing must be complete
        // i didn't use AlreadyInProgress() cuz uncompiled complete
        // source files are also considered in progress
        // - the only thing i want to check here is that
        //   the file is not still being written to
        // @TODO it sounds like this checks only
        // in this JVM, is it a problem ? (are open
        // files locked by default ?? )
        assert !onProgress.contains(generatedClassName);
        
        // BARRIER
        // don't compile unless all source files are generated
        // i.e. nothing exits in the onProgress list
        // - does this really mean anything ?
        // - the only case i would enter the barrier
        // - is when another class depends totally on me
        // - and that class is in this JVM ( since same static var onProgress )
        // - and since no threads in SRMI ( currently ) then
        // - this barrier is absolutely useless
        if(onProgress.size() > 0 && !classKeyword.equals("interface"))
        {
            try
            {
                l.info("Entring source-code barrier waiting for sources to be written");
                Thread.sleep(1000);
            }
            catch (InterruptedException ex)
            {
                //ex.printStackTrace(); 
            }
            if(onProgress.size() > 0)
                for(String s : onProgress)
                    l.error("Stuck source file " + s);
        }
        
        synchronized (getWrappedClass())
        {
            
            l.trace("Compiling " + generatedClassName + ".java" + "...");
            
            // only compile files that was generated after the last update of the dependency class file
            final long afterTime = new File(fullPathOfDependencyClassFile).lastModified();
            
            String[] all_dependant_files = new File(
                    destinationFolder + package_.replace(".", System.getProperty("file.separator")) +
                    java.lang.System.getProperty("file.separator")
                    ).
                    list(new FilenameFilter()
            {
                public boolean accept(File dir, String name)
                {
                    return name.startsWith(wrappedUnQualifiedClassName) 
                    && name.endsWith("java") 
                    && new File(dir.getPath() + System.getProperty("file.separator") + name).lastModified() > afterTime;
                }
            });
            
            String[] args = new String[] {
                "-classpath",classPath +
                        (System.getProperty("srmi.extra.libs") != null ? System.getProperty("path.separator") : "") +
                        (System.getProperty("srmi.extra.libs") != null ? System.getProperty("srmi.extra.libs") : ""),
                "-d",destinationFolder,
                /*fullPathToSourceFileName*/
            } ;
            
            ArrayList<String> final_args = new ArrayList<String>();
            for(String s : args)
                final_args.add(s);
            for(String s : all_dependant_files)
                final_args.add(destinationFolder + package_.replace(".", System.getProperty("file.separator")) +
                    java.lang.System.getProperty("file.separator") + s);
            
            System.out.print("javac ");
            for(String s : final_args)
            {
                System.out.print(s + " ");
            }
            System.out.println();
            
            int errCode = -1;
            
            GetLock();
            
            errCode = com.sun.tools.javac.Main.compile(
                    final_args.toArray(new String[]{}),
                    new PrintWriter(System.out));
            
            ReleaseLock();
            
            if(errCode == 0)
            {
                try
                {
                    Class ret = loader.loadClass(package_ + "." + generatedClassName);
                        //Class.forName(package_ + "." + generatedClassName);
                    l.trace("Success, loaded compiled class file");
                    return ret;
                }
                catch (ClassNotFoundException ex)
                {
                    l.error("Javac returned 0 for success, but can't load class (classpath issue ?)");
                    return null;
                }
            }
            else
            {
                l.info("Failed; javac returned " + errCode);
            }
        }
        assert false : "should never reach here"; // should never reach here
        System.out.println("SHOULD NEVER REACH HERE ClassGenerator");
        System.exit(1);
        return null;
    }
    
    /*private void setupClassPath()
    {
        URLClassLoader sysloader = (URLClassLoader)ClassLoader.getSystemClassLoader();
        Class sysclass = URLClassLoader.class;
     
        try {
                URL newClassPathURL = new File(classesFolder).toURL();
                Method method = sysclass.getDeclaredMethod("addURL", new Class[]{URL.class});
                method.setAccessible(true);
                method.invoke(sysloader,new Object[]{ newClassPathURL });
                l.info("Set the classpath = " + newClassPathURL.toString());
        } catch (Throwable t) {
                t.printStackTrace();
        }
     
    }*/
    
    protected String classPath;
    protected String classesFolder;
    protected String destinationFolder;
    protected String fullPathToSourceFileName;
    protected String generatedClassName;
    protected String package_;
    protected PrintWriter file;
    protected String wrappedUnQualifiedClassName;
    protected String wrappedFullyQualifiedClassName;
    protected String fullPathOfGeneratedClassFile;
    private Class wrappedClass;
    protected String classKeyword = "class";
    
    protected abstract void PrepareEmit() throws SRMIException;
    protected abstract void EmitSupers() throws SRMIException;
    protected abstract void EmitConstructor() throws SRMIException;
    protected abstract void EmitFunction(Method m) throws SRMIException;
    protected abstract void EmitMembersVariables() throws SRMIException;
    protected abstract String getGeneratedClassNameSuffix() throws SRMIException;
    
    protected void EmitPackage()
    {
        file.println(String.format("package %s;\n",package_ ));
    }
    
    protected void EmitClass() throws SRMIException
    {
        file.print(String.format("public %s %s ",classKeyword,this.generatedClassName));
        EmitSupers();
        file.print("\n{\n");
        
        EmitMembersVariables();
        EmitConstructor();
        EmitFunctions();
        
        file.println("}\n");
    }
    
    protected void EmitFunctions() throws SRMIException
    {
        for(Method m : getWrappedClass().getDeclaredMethods())
            EmitFunction(m);
    }
    
    protected void EmitModifiers(int mod)
    {
        if (Modifier.isPublic(mod))
            file.print("\tpublic");
        else if (Modifier.isProtected(mod))
            file.print("\tprotected");
        else if (Modifier.isPrivate(mod))
        {
            l.warn("Emitting the modifier 'private'");
            file.print("\tprivate");
        }
        else
            file.print("\t");
    }
    
    protected void EmitArgs(Method m, boolean noEmitType)
    {
        Class[] params = m.getParameterTypes();
        for (int i = 0; i < params.length; i ++)
        {
            EmitArg(params[i], noEmitType, i);
            if(i != params.length - 1)
            {
                file.print(", ");
            }
        }
    }
    
    protected void EmitArg(Class<?> param, boolean noEmitType, int number)
    {
        if(!noEmitType)
        {
            EmitType(param);
            file.print(" ");
        }
        file.print("p" + number); // param name
    }
    
    protected void EmitExceptions(Class[] exceps)
    {
        if(exceps.length > 0)
            file.print(" throws ");
        for (int i = 0; i < exceps.length; i ++)
        {
            EmitType(exceps[i]);
            if(i != exceps.length - 1)
            {
                file.print(", ");
            }
        }
    }
    
    protected void EmitType(Class type)
    {
        file.print(type.getCanonicalName());
    }
    
    protected void EmitTryHeader()
    {
        file.print("\t\ttry\n" +
                "\t\t{\n");
    }
    
    protected void EmitCatchHeader(Class exceptionClass)
    {
        file.print(String.format("\t\t} catch (%s ex) { \n",exceptionClass.getCanonicalName()));
    }
    
    protected void PrepareDefaultWrapperEmit()
    {
        try
        {
            wrappedFullyQualifiedClassName
                    = getWrappedClass().getCanonicalName();
            package_
                    = wrappedFullyQualifiedClassName.substring(0,wrappedFullyQualifiedClassName.lastIndexOf('.'));
            wrappedUnQualifiedClassName
                    = wrappedFullyQualifiedClassName.substring(wrappedFullyQualifiedClassName.lastIndexOf('.')+1);
            generatedClassName
                    = wrappedUnQualifiedClassName + getGeneratedClassNameSuffix();
            classesFolder
                    = System.getProperty("srmi.basedir")/*new File(".").getCanonicalPath()*/ + System.getProperty("file.separator");
            destinationFolder
                    =  classesFolder;
            fullPathToSourceFileName
                    = destinationFolder + package_.replace(".",System.getProperty("file.separator")) + System.getProperty("file.separator") + generatedClassName + ".java";
            fullPathOfGeneratedClassFile = destinationFolder +
                    package_.replace(".",System.getProperty("file.separator")) +
                    System.getProperty("file.separator") +
                    generatedClassName +
                    ".class";
            classPath
                    = classesFolder + 
                    // System.getProperty("path.separator") + System.getProperty("srmi.jar.path") + // i probly don't need to add it cuz it is already in the classpath
                    System.getProperty("path.separator") +
                    System.getProperty("java.class.path");
        }
        catch (Exception ex)
        {
            l.error("",ex);
        }
    }
  
    // prepare the package folder
    private void PrepareFolderHeirarchy() throws SRMIException 
    {
        String absdir = destinationFolder + package_.replace(".",System.getProperty("file.separator"));
        if(new File(absdir).exists())
            return;
        String accumulativeDir = "";
        for(String dir : absdir.split(System.getProperty("file.separator")))
        {
            if(dir.length() < 1)
                continue;
            
            accumulativeDir += System.getProperty("file.separator") + dir;
            File file = new File(accumulativeDir);
            
            if(file.exists())
                continue;           
            
            if(!file.mkdir())
            {
                l.fatal("Can't create " + accumulativeDir);
                throw new SRMIException("Can't create destination folder (" + accumulativeDir + ") of the generated class");
            }
        }
        l.info("Created dir: " + accumulativeDir);
    }
    
    private void OpenFile()
    {
        try
        {
            File outputFile =  new File(fullPathToSourceFileName);
            
            if(outputFile.exists() && outputFile.length() > 0 /*zero-length files are overwritable*/)
            {
                l.warn("File already exists " + fullPathToSourceFileName);
                return;
            }
            else if(!outputFile.exists())
            {
                try
                {                    
                    outputFile.createNewFile();
                }
                catch(IOException ex)
                {
                    l.error("Exception creating file " + outputFile.toString(),ex);
                    return;
                }
            }// else, it exists, and it's size is zero, the wanted
            
            GetLock(outputFile);
            
            l.trace("Creating PrintWriter to " + fullPathToSourceFileName);
            file = new PrintWriter(new File(fullPathToSourceFileName));
        }
        catch (Exception ex)
        {
            l.error("",ex);
        }
    }
    
    private void CloseFile()
    {
        file.close();
        
        ReleaseLock();
    }
    
    public Class getWrappedClass()
    {
        return wrappedClass;
    }
    
    public void setWrappedClass(Class wrappedClass)
    {
        this.wrappedClass = wrappedClass;
    }
    
    private boolean AlreadyInProgress()
    {
        synchronized (onProgress)
        {
            boolean yes = onProgress.contains(generatedClassName)
            || // cross-jvm check
                    (new File(fullPathToSourceFileName).exists()
                    && !new File(fullPathOfGeneratedClassFile).exists()
                    );
            
            if(yes)
                l.info("Already in progress : " + generatedClassName +
                        (onProgress.contains(generatedClassName)?
                            " in the current JVM":
                            " still uncompiled source (by another JVM maybe), or already compiled and existing"));
            
            return yes;
        }
    }
    
    private void AddInProgress()
    {
        l.info("Adding " + generatedClassName + " to onProgress");
        onProgress.add(generatedClassName);
    }
    
    private void RemoveFromProgress()
    {
        l.info("Removing " + generatedClassName + " to onProgress");
        l.info(generatedClassName + " finished generation");
        onProgress.remove(generatedClassName);
    }
    
    private void ReleaseLock()
    {
        ReleaseLock(new File("javac_lock"));
    }
    
    private void ReleaseLock(/*unused*/ File file)
    {
        assert lock != null;
        if(lock == null)
        {
            l.error("Attempting to release a null lock " + file.getName());
            return;
        }
        
        try
        {
            // exit system-wide lock
            lock.release();
            lock = null;
            l.info(file.getName() + " lock released");
        }
        catch (IOException ex)
        {
            l.error("Exception while releasing lock",ex);
        }
    }
    
    private void GetLock(File file)
    {
        assert lock == null;
        if(lock != null)
            l.warn("Getting more than one lock in a single class generator " + file.getName());
        
        try
        {
            // enter system-wide lock
            l.info("Getting " + file.getName() + " lock");
            
            while(lock==null)
            {
                lock = new RandomAccessFile(file,"rw").getChannel().tryLock();
                Thread.sleep(100);
            }
            l.info(file.getName() + " lock obtained");
        }
        catch (Exception ex )
        {
            l.error("Exception while getting lock",ex);
        }
    }
    
    private void GetLock()
    {
        GetLock(new File("javac_lock"));
    }
    
    private void DeleteOld(File file)
    {
        if(!file.exists())
        {
            l.warn("Was asked to delete a non-existent file " + file.getName());
            return;
        }
        if(file.delete())
        {
            l.info("Deleted old " + file.getName() );
        }
        else
        {
            l.warn("Can't delete old " + file.getName());
        }
    }
    
}
